﻿namespace NJQuery {
	using System;
	using System.ComponentModel;
	using System.Globalization;

	/// <summary>
	/// Defines the duration of an animation.
	/// </summary>
	[TypeConverter(typeof(DurationConverter))]
	public struct Duration : IEquatable<Duration>, IComplexProperty {


		#region Static Readonly Fields: Standard values

		/// <summary>
		/// Instructs jQuery UI not to animate the action taken.
		/// </summary>
		public static readonly Duration Immediate = new Duration();

		/// <summary>
		/// Instructs jQuery UI to use the duration associated with the predefined "normal" animaiton speed.
		/// </summary>
		public static readonly Duration Normal = new Duration(NJQuery.Speed.Normal);

		/// <summary>
		/// Instructs jQuery UI to use the duration associated with the predefined "fast" animaiton speed.
		/// </summary>
		public static readonly Duration Fast = new Duration(NJQuery.Speed.Fast);

		/// <summary>
		/// Instructs jQuery UI to use the duration associated with the predefined "slow" animaiton speed.
		/// </summary>
		public static readonly Duration Slow = new Duration(NJQuery.Speed.Slow);

		#endregion


		#region Static Methods

		/// <summary>
		/// Converts a string representation to its <see cref="Duration" /> equivalent.
		/// </summary>
		/// <param name="text">A string containing the value to convert.</param>
		/// <param name="result">The parsed value when the conversion succeeds, or a default instance otherwise.</param>
		/// <returns>Indicates whether the conversion succeeded.</returns>
		public static bool TryParse(string text, out Duration result) {

			if(string.IsNullOrEmpty(text)) {
				result = Immediate;
				return true;
			}

			int intValue;
			if(int.TryParse(text, out intValue)) {
				result = new Duration(intValue);
				return true;
			}

			//TODO: Write a Enum.TryParse!!!
			Speed speed;
			try {
				speed = (Speed)Enum.Parse(typeof(Speed), text);
			} catch(ArgumentException) {
				result = Immediate;
				return false;
			}

			result = new Duration(speed);
			return true;
		}

		#endregion


		#region Fields

		private readonly Speed? speed;
		private readonly int? milliseconds;

		#endregion


		#region Constructors

		/// <summary>
		/// Initializes a new <see cref="Duration" /> value using the value associated with a predefined animation speed.
		/// </summary>
		/// <param name="speed">The associated predefined animation speed to use.</param>
		/// <exception cref="ArgumentException">Speed value not defined.</exception>
		public Duration(Speed speed) {
			if(!Enum.IsDefined(typeof(Speed), speed)) throw new ArgumentException("Value not defined.", "speed");

			this.speed = speed;
			milliseconds = null;
		}

		/// <summary>
		/// Initializes a new <see cref="Duration" /> value using milliseconds.
		/// </summary>
		/// <param name="milliseconds">The fixed number of milliseconds the animation should span.</param>
		/// <exception cref="ArgumentOutOfRangeException">Milliseconds value must be non-negative.</exception>
		public Duration(int milliseconds) {
			if(milliseconds < 0) throw new ArgumentOutOfRangeException("milliseconds", "Value must be non-negative.");

			speed = null;
			this.milliseconds = milliseconds;
		}

		#endregion


		#region Properties

		/// <summary>
		/// If applicable, the associated predefined animation speed to use.
		/// </summary>
		public Speed? Speed {
			get { return speed; }
		}

		/// <summary>
		/// If applicable, the fixed number of milliseconds the animation should span.
		/// </summary>
		public int? Milliseconds {
			get { return milliseconds; }
		}

		/// <summary>
		/// Whether or not a predefined speed or fixed duration has been assigned.
		/// </summary>
		public bool HasValue {
			get { return speed.HasValue || milliseconds.HasValue; }
		}

		#endregion


		#region Methods: Object management

		/// <summary>
		/// Determines whether or not the specified object is equal to the current <see cref="Duration" /> value.
		/// </summary>
		/// <param name="obj">The <see cref="Object"/> to compare.</param>
		/// <returns>Whether or not the provided object is equal.</returns>
		public override bool Equals(object obj) {
			if(obj is Duration) return this == (Duration)obj;
			return false;
		}

		/// <summary>
		/// Determines whether or not the specified Duration is equal to the current <see cref="Duration" /> value.
		/// </summary>
		/// <param name="other">The <see cref="Duration" /> to compare.</param>
		/// <returns>Whether or not the provided value is equal.</returns>
		public bool Equals(Duration other) {
			return this == other;
		}

		/// <summary>
		/// Serves as a hash function for <see cref="Duration" />.
		/// </summary>
		/// <returns>The hash value associated with the contained data.</returns>
		public override int GetHashCode() {
			if(milliseconds.HasValue)
				return milliseconds.Value;
			else if(speed.HasValue)
				return ~(int)speed.Value; // Normal: -1, Slow: -2, Fast: -3
			else
				return int.MinValue;
		}

		/// <summary>
		/// Returns a <see cref="String" /> that represents the current <see cref="Duration" /> value.
		/// </summary>
		/// <returns>The string representation.</returns>
		public override string ToString() {
			if(speed.HasValue) return speed.Value.ToString();
			if(milliseconds.HasValue) return milliseconds.Value.ToString(CultureInfo.InvariantCulture);
			return string.Empty;
		}

		/// <summary>
		/// Returns a literal JavaScript value used to represent the server value on the client.
		/// </summary>
		/// <returns>The client script value.</returns>
		public string ToClientSource() {
			if(speed.HasValue) return speed.Value.ToClientSource();
			if(milliseconds.HasValue) return milliseconds.Value.ToClientSource();
			return ScriptBuilder.NullLiteral;
		}

		#endregion


		#region Operators

		/// <summary>
		/// Compares the equality of two <see cref="Duration" /> values.
		/// </summary>
		/// <param name="value0">The first value.</param>
		/// <param name="value1">The second value.</param>
		/// <returns>Whether or not the two values are equal.</returns>
		public static bool operator ==(Duration value0, Duration value1) {
			if(!value0.HasValue && !value1.HasValue) return true;
			if(value0.speed.HasValue && value1.speed.HasValue) return value0.speed.Value == value1.speed.Value;
			if(value0.milliseconds.HasValue && value1.milliseconds.HasValue) return value0.milliseconds.Value == value1.milliseconds.Value;
			return false;
		}

		/// <summary>
		/// Compares the inequality of two <see cref="Duration" /> values.
		/// </summary>
		/// <param name="value0">The first value.</param>
		/// <param name="value1">The second value.</param>
		/// <returns>Whether or not the two values are distinct.</returns>
		public static bool operator !=(Duration value0, Duration value1) {
			if(!value0.HasValue && !value1.HasValue) return false;
			if(value0.speed.HasValue && value1.speed.HasValue) return value0.speed.Value != value1.speed.Value;
			if(value0.milliseconds.HasValue && value1.milliseconds.HasValue) return value0.milliseconds.Value != value1.milliseconds.Value;
			return true;
		}

		#endregion


	}

}